package net.sf.fmj.ui.wizard;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.Insets;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.border.EmptyBorder;

/**
 * 
 * Adapted Robert Eckstein's sample at
 * http://java.sun.com/developer/technicalArticles/GUI/swing/wizard/
 * 
 * This class implements a basic wizard dialog, where the programmer can insert
 * one or more Components to act as panels. These panels can be navigated
 * through arbitrarily using the 'Next' or 'Back' buttons, or the dialog itself
 * can be closed using the 'Cancel' button. Note that even though the dialog
 * uses a CardLayout manager, the order of the panels is not linear. Each panel
 * determines at runtime what its next and previous panel will be.
 */
public class Wizard extends WindowAdapter implements PropertyChangeListener {

	/**
	 * Indicates that the 'Finish' button was pressed to close the dialog.
	 */
	public static final int FINISH_RETURN_CODE = 0;
	/**
	 * Indicates that the 'Cancel' button was pressed to close the dialog, or
	 * the user pressed the close box in the corner of the window.
	 */
	public static final int CANCEL_RETURN_CODE = 1;
	/**
	 * Indicates that the dialog closed due to an internal error.
	 */
	public static final int ERROR_RETURN_CODE = 2;

	/**
	 * The String-based action command for the 'Next' button.
	 */
	public static final String NEXT_BUTTON_ACTION_COMMAND = "NextButtonActionCommand";
	/**
	 * The String-based action command for the 'Back' button.
	 */
	public static final String BACK_BUTTON_ACTION_COMMAND = "BackButtonActionCommand";
	/**
	 * The String-based action command for the 'Cancel' button.
	 */
	public static final String CANCEL_BUTTON_ACTION_COMMAND = "CancelButtonActionCommand";

	// The i18n text used for the buttons. Loaded from a property resource file.

	static String BACK_TEXT = "< Back";
	static String NEXT_TEXT = "Next >";
	static String FINISH_TEXT = "Finish";
	static String CANCEL_TEXT = "Cancel";

	// The image icons used for the buttons. Filenames are loaded from a
	// property resource file.

	// static Icon BACK_ICON;
	// static Icon NEXT_ICON;
	// static Icon FINISH_ICON;
	// static Icon CANCEL_ICON;

	private WizardModel wizardModel;
	private WizardController wizardController;
	private JDialog wizardDialog;

	private JPanel cardPanel;
	private CardLayout cardLayout;
	private JButton backButton;
	private JButton nextButton;
	private JButton cancelButton;

	private int returnCode;

	/**
	 * Default constructor. This method creates a new WizardModel object and
	 * passes it into the overloaded constructor.
	 */
	// public Wizard() {
	// this((Frame)null);
	// }

	/**
	 * This method accepts a java.awt.Dialog object as the javax.swing.JDialog's
	 * parent.
	 * 
	 * @param owner
	 *            The java.awt.Dialog object that is the owner of this dialog.
	 */
	public Wizard(Dialog owner) {
		wizardModel = new WizardModel();
		wizardDialog = new JDialog(owner);
		initComponents();
	}

	/**
	 * This method accepts a java.awt.Frame object as the javax.swing.JDialog's
	 * parent.
	 * 
	 * @param owner
	 *            The java.awt.Frame object that is the owner of the
	 *            javax.swing.JDialog.
	 */
	public Wizard(Frame owner) {
		wizardModel = new WizardModel();
		wizardDialog = new JDialog(owner);
		initComponents();
	}

	/**
	 * Returns an instance of the JDialog that this class created. This is
	 * useful in the event that you want to change any of the JDialog parameters
	 * manually.
	 * 
	 * @return The JDialog instance that this class created.
	 */
	public JDialog getDialog() {
		return wizardDialog;
	}

	/**
	 * Returns the owner of the generated javax.swing.JDialog.
	 * 
	 * @return The owner (java.awt.Frame or java.awt.Dialog) of the
	 *         javax.swing.JDialog generated by this class.
	 */
	public Component getOwner() {
		return wizardDialog.getOwner();
	}

	/**
	 * Sets the title of the generated javax.swing.JDialog.
	 * 
	 * @param s
	 *            The title of the dialog.
	 */
	public void setTitle(String s) {
		wizardDialog.setTitle(s);
	}

	/**
	 * Returns the current title of the generated dialog.
	 * 
	 * @return The String-based title of the generated dialog.
	 */
	public String getTitle() {
		return wizardDialog.getTitle();
	}

	/**
	 * Sets the modality of the generated javax.swing.JDialog.
	 * 
	 * @param b
	 *            the modality of the dialog
	 */
	public void setModal(boolean b) {
		wizardDialog.setModal(b);
	}

	/**
	 * Returns the modality of the dialog.
	 * 
	 * @return A boolean indicating whether or not the generated
	 *         javax.swing.JDialog is modal.
	 */
	public boolean isModal() {
		return wizardDialog.isModal();
	}

	/**
	 * Convienence method that displays a modal wizard dialog and blocks until
	 * the dialog has completed.
	 * 
	 * @return Indicates how the dialog was closed. Compare this value against
	 *         the RETURN_CODE constants at the beginning of the class.
	 */
	public int showModalDialog() {

		wizardDialog.setModal(true);
		wizardDialog.pack();
		wizardDialog.setLocationRelativeTo(wizardDialog.getOwner());
		wizardDialog.setVisible(true);

		return returnCode;
	}

	/**
	 * Returns the current model of the wizard dialog.
	 * 
	 * @return A WizardModel instance, which serves as the model for the wizard
	 *         dialog.
	 */
	public WizardModel getModel() {
		return wizardModel;
	}

	/**
	 * Add a Component as a panel for the wizard dialog by registering its
	 * WizardPanelDescriptor object. Each panel is identified by a unique
	 * Object-based identifier (often a String), which can be used by the
	 * setCurrentPanel() method to display the panel at runtime.
	 * 
	 * @param id
	 *            An Object-based identifier used to identify the
	 *            WizardPanelDescriptor object.
	 * @param panel
	 *            The WizardPanelDescriptor object which contains helpful
	 *            information about the panel.
	 */
	public void registerWizardPanel(Object id, WizardPanelDescriptor panel) {

		// Add the incoming panel to our JPanel display that is managed by
		// the CardLayout layout manager.

		cardPanel.add(panel.getPanelComponent(), id);

		// Set a callback to the current wizard.

		panel.setWizard(this);

		// Place a reference to it in the model.

		wizardModel.registerPanel(id, panel);

	}

	/**
	 * Displays the panel identified by the object passed in. This is the same
	 * Object-based identified used when registering the panel.
	 * 
	 * @param id
	 *            The Object-based identifier of the panel to be displayed.
	 */
	public void setCurrentPanel(Object id) {

		// Get the hashtable reference to the panel that should
		// be displayed. If the identifier passed in is null, then close
		// the dialog.

		if (id == null)
			close(ERROR_RETURN_CODE);

		WizardPanelDescriptor oldPanelDescriptor = wizardModel
				.getCurrentPanelDescriptor();
		if (oldPanelDescriptor != null) {
			if (!oldPanelDescriptor.aboutToHidePanel(id))
				return; // transition rejected
		}

		Object oldId = oldPanelDescriptor != null ? oldPanelDescriptor
				.getPanelDescriptorIdentifier() : null;
		if (!wizardModel.getPanelDescriptor(id).aboutToDisplayPanel(oldId))
			return; // transition rejected

		wizardModel.setCurrentPanel(id);

		// Show the panel in the dialog.

		cardLayout.show(cardPanel, id.toString());
		wizardModel.getCurrentPanelDescriptor().displayingPanel();

	}

	/**
	 * Method used to listen for property change events from the model and
	 * update the dialog's graphical components as necessary.
	 * 
	 * @param evt
	 *            PropertyChangeEvent passed from the model to signal that one
	 *            of its properties has changed value.
	 */
	public void propertyChange(PropertyChangeEvent evt) {

		if (evt.getPropertyName().equals(
				WizardModel.CURRENT_PANEL_DESCRIPTOR_PROPERTY)) {
			wizardController.resetButtonsToPanelRules();
		} else if (evt.getPropertyName().equals(
				WizardModel.NEXT_FINISH_BUTTON_TEXT_PROPERTY)) {
			nextButton.setText(evt.getNewValue().toString());
		} else if (evt.getPropertyName().equals(
				WizardModel.BACK_BUTTON_TEXT_PROPERTY)) {
			backButton.setText(evt.getNewValue().toString());
		} else if (evt.getPropertyName().equals(
				WizardModel.CANCEL_BUTTON_TEXT_PROPERTY)) {
			cancelButton.setText(evt.getNewValue().toString());
		} else if (evt.getPropertyName().equals(
				WizardModel.NEXT_FINISH_BUTTON_ENABLED_PROPERTY)) {
			nextButton.setEnabled(((Boolean) evt.getNewValue()).booleanValue());
		} else if (evt.getPropertyName().equals(
				WizardModel.BACK_BUTTON_ENABLED_PROPERTY)) {
			backButton.setEnabled(((Boolean) evt.getNewValue()).booleanValue());
		} else if (evt.getPropertyName().equals(
				WizardModel.CANCEL_BUTTON_ENABLED_PROPERTY)) {
			cancelButton.setEnabled(((Boolean) evt.getNewValue())
					.booleanValue());
		} else if (evt.getPropertyName().equals(
				WizardModel.NEXT_FINISH_BUTTON_ICON_PROPERTY)) {
			nextButton.setIcon((Icon) evt.getNewValue());
		} else if (evt.getPropertyName().equals(
				WizardModel.BACK_BUTTON_ICON_PROPERTY)) {
			backButton.setIcon((Icon) evt.getNewValue());
		} else if (evt.getPropertyName().equals(
				WizardModel.CANCEL_BUTTON_ICON_PROPERTY)) {
			cancelButton.setIcon((Icon) evt.getNewValue());
		}

	}

	/**
	 * Retrieves the last return code set by the dialog.
	 * 
	 * @return An integer that identifies how the dialog was closed. See the
	 *         *_RETURN_CODE constants of this class for possible values.
	 */
	public int getReturnCode() {
		return returnCode;
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @return A boolean indicating if the button is enabled.
	 */
	public boolean getBackButtonEnabled() {
		return wizardModel.getBackButtonEnabled().booleanValue();
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @param newValue
	 *            The new enabled status of the button.
	 */
	public void setBackButtonEnabled(boolean newValue) {
		wizardModel.setBackButtonEnabled(new Boolean(newValue));
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @return A boolean indicating if the button is enabled.
	 */
	public boolean getNextFinishButtonEnabled() {
		return wizardModel.getNextFinishButtonEnabled().booleanValue();
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @param newValue
	 *            The new enabled status of the button.
	 */
	public void setNextFinishButtonEnabled(boolean newValue) {
		wizardModel.setNextFinishButtonEnabled(new Boolean(newValue));
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @return A boolean indicating if the button is enabled.
	 */
	public boolean getCancelButtonEnabled() {
		return wizardModel.getCancelButtonEnabled().booleanValue();
	}

	/**
	 * Mirrors the WizardModel method of the same name.
	 * 
	 * @param newValue
	 *            The new enabled status of the button.
	 */
	public void setCancelButtonEnabled(boolean newValue) {
		wizardModel.setCancelButtonEnabled(new Boolean(newValue));
	}

	/**
	 * Closes the dialog and sets the return code to the integer parameter.
	 * 
	 * @param code
	 *            The return code.
	 */
	void close(int code) {

		WizardPanelDescriptor oldPanelDescriptor = wizardModel
				.getCurrentPanelDescriptor();
		if (oldPanelDescriptor != null && code == Wizard.FINISH_RETURN_CODE) {
			if (!oldPanelDescriptor
					.aboutToHidePanel(WizardPanelDescriptor.FINISH))
				return; // transition rejected
		}

		returnCode = code;
		wizardDialog.dispose();
	}

	/**
	 * This method initializes the components for the wizard dialog: it creates
	 * a JDialog as a CardLayout panel surrounded by a small amount of space on
	 * each side, as well as three buttons at the bottom.
	 */

	private void initComponents() {

		wizardModel.addPropertyChangeListener(this);
		wizardController = new WizardController(this);

		wizardDialog.getContentPane().setLayout(new BorderLayout());
		wizardDialog.addWindowListener(this);

		// Create the outer wizard panel, which is responsible for three
		// buttons:
		// Next, Back, and Cancel. It is also responsible a JPanel above them
		// that
		// uses a CardLayout layout manager to display multiple panels in the
		// same spot.

		JPanel buttonPanel = new JPanel();
		JSeparator separator = new JSeparator();
		Box buttonBox = new Box(BoxLayout.X_AXIS);

		cardPanel = new JPanel();
		cardPanel.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10)));

		cardLayout = new CardLayout();
		cardPanel.setLayout(cardLayout);

		backButton = new JButton();
		nextButton = new JButton();
		cancelButton = new JButton();

		backButton.setActionCommand(BACK_BUTTON_ACTION_COMMAND);
		nextButton.setActionCommand(NEXT_BUTTON_ACTION_COMMAND);
		cancelButton.setActionCommand(CANCEL_BUTTON_ACTION_COMMAND);

		backButton.addActionListener(wizardController);
		nextButton.addActionListener(wizardController);
		cancelButton.addActionListener(wizardController);

		// Create the buttons with a separator above them, then place them
		// on the east side of the panel with a small amount of space between
		// the back and the next button, and a larger amount of space between
		// the next button and the cancel button.

		buttonPanel.setLayout(new BorderLayout());
		buttonPanel.add(separator, BorderLayout.NORTH);

		buttonBox.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10)));
		buttonBox.add(backButton);
		buttonBox.add(Box.createHorizontalStrut(10));
		buttonBox.add(nextButton);
		buttonBox.add(Box.createHorizontalStrut(30));
		buttonBox.add(cancelButton);

		buttonPanel.add(buttonBox, java.awt.BorderLayout.EAST);

		wizardDialog.getContentPane().add(buttonPanel,
				java.awt.BorderLayout.SOUTH);
		wizardDialog.getContentPane().add(cardPanel,
				java.awt.BorderLayout.CENTER);

	}

	/**
	 * If the user presses the close box on the dialog's window, treat it as a
	 * cancel.
	 * 
	 * @param e
	 *            The event passed in from AWT.
	 */

	public void windowClosing(WindowEvent e) {
		returnCode = CANCEL_RETURN_CODE;
	}

}
